package com.netflix.client.ssl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;

/**
 * Secure socket factory that is used the NIWS code if a non-standard key store or trust store
 * is specified.
 */
public class URLSslContextFactory extends AbstractSslContextFactory {

    private final static Logger LOGGER = LoggerFactory.getLogger(URLSslContextFactory.class);

    private final URL keyStoreUrl;
    private final URL trustStoreUrl;

    /**
     * Creates a {@code ClientSSLSocketFactory} instance. This instance loads only the given trust
     * store file and key store file. Both trust store and key store must be protected by passwords,
     * even though it is not mandated by JSSE.
     *
     * @param trustStoreUrl      A {@link URL} that points to a trust store file. If non-null, this URL
     *                           must refer to a JKS key store file that contains trusted certificates.
     * @param trustStorePassword The password of the given trust store file. If a trust store is
     *                           specified, then the password may not be empty.
     * @param keyStoreUrl        A {@code URL} that points to a key store file that contains both client
     *                           certificate and the client's private key. If non-null, this URL must be of JKS format.
     * @param keyStorePassword   the password of the given key store file. If a key store is
     *                           specified, then the password may not be empty.
     * @throws ClientSslSocketFactoryException thrown if creating this instance fails.
     */
    public URLSslContextFactory(final URL trustStoreUrl, final String trustStorePassword, final URL keyStoreUrl, final String keyStorePassword) throws ClientSslSocketFactoryException {
        super(createKeyStore(trustStoreUrl, trustStorePassword), trustStorePassword, createKeyStore(keyStoreUrl, keyStorePassword), keyStorePassword);

        this.keyStoreUrl = keyStoreUrl;
        this.trustStoreUrl = trustStoreUrl;

        LOGGER.info("Loaded keyStore from: {}", keyStoreUrl);
        LOGGER.info("loaded trustStore from: {}", trustStoreUrl);
    }

    /**
     * Opens the specified key or trust store using the given password.
     * <p>
     * In case of failure {@link com.netflix.client.ssl.ClientSslSocketFactoryException} is thrown, and wrapps the
     * underlying cause exception. That could be:
     * <ul>
     *     <li>KeyStoreException if the JRE doesn't support the standard Java Keystore format, in other words: never</li>
     *     <li>NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found</li>
     *     <li>CertificateException if any of the certificates in the keystore could not be loaded</li>
     *     <li>
     *         IOException if there is an I/O or format problem with the keystore data, if a
     *         password is required but not given, or if the given password was incorrect. If the
     *         error is due to a wrong password, the cause of the IOException should be an UnrecoverableKeyException.
     *     </li>
     * </ul>
     *
     * @param storeFile the location of the store to load
     * @param password  the password protecting the store
     * @return the newly loaded key store
     * @throws ClientSslSocketFactoryException a wrapper exception for any problems encountered during keystore creation.
     */
    private static KeyStore createKeyStore(final URL storeFile, final String password) throws ClientSslSocketFactoryException {
        if (storeFile == null) {
            return null;
        }

        Preconditions.checkArgument(StringUtils.isNotEmpty(password), "Null keystore should have empty password, defined keystore must have password");

        KeyStore keyStore = null;

        try {
            keyStore = KeyStore.getInstance("jks");

            InputStream is = storeFile.openStream();

            try {
                keyStore.load(is, password.toCharArray());
            } catch (NoSuchAlgorithmException e) {
                throw new ClientSslSocketFactoryException(String.format("Failed to create a keystore that supports algorithm %s: %s", SOCKET_ALGORITHM, e.getMessage()), e);
            } catch (CertificateException e) {
                throw new ClientSslSocketFactoryException(String.format("Failed to create keystore with algorithm %s due to certificate exception: %s", SOCKET_ALGORITHM, e.getMessage()), e);
            } finally {
                try {
                    is.close();
                } catch (IOException ignore) { // NOPMD
                }
            }
        } catch (KeyStoreException e) {
            throw new ClientSslSocketFactoryException(String.format("KeyStore exception creating keystore: %s", e.getMessage()), e);
        } catch (IOException e) {
            throw new ClientSslSocketFactoryException(String.format("IO exception creating keystore: %s", e.getMessage()), e);
        }
        return keyStore;
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder();

        builder.append("ClientSslSocketFactory [trustStoreUrl=").append(trustStoreUrl);
        if (trustStoreUrl != null) {
            builder.append(", trustStorePassword=");
            builder.append(Strings.repeat("*", this.getTrustStorePasswordLength()));
        }
        builder.append(", keyStoreUrl=").append(keyStoreUrl);
        if (keyStoreUrl != null) {
            builder.append(", keystorePassword = ");
            builder.append(Strings.repeat("*", this.getKeyStorePasswordLength()));
        }
        builder.append(']');

        return builder.toString();
    }
}
